package com.jackstraw.jack_base.datasource.redis.util;


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.jackstraw.jack_base.datasource.common.entity.enums.RedisTypeEnum;
import com.jackstraw.jack_base.datasource.redis.bean.RedisPropertiesEntity;
import com.jackstraw.jack_base.datasource.redis.exception.BadRedisException;
import com.jackstraw.jack_base.util.JsonUtil;
import com.jackstraw.jack_base.util.ObjectUtil;
import com.jackstraw.jack_base.util.SpringUtil;
import com.jackstraw.jack_base.util.StringUtil;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.resource.ClientResources;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.api.RedissonRxClient;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.Assert;

import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;


/**
 * redis连接配置类
 */
@Slf4j
public class RedisConnectionUtil {

    private static final String REDIS_PROTOCOL_PREFIX = "redis://";
    private static final String REDISS_PROTOCOL_PREFIX = "rediss://";

    /*----------RedisConfiguration start----------*/

    public static RedisConfiguration redisConfiguration(RedisPropertiesEntity properties) {
        RedisConfiguration redisConfiguration = null;
        switch (properties.getRedisTypeEnum()) {
            case Single:
                redisConfiguration = standaloneConfig(properties);
                break;
            case Cluster:
                redisConfiguration = clusterConfiguration(properties);
                break;
            case MasterSlave:
                redisConfiguration = staticMasterReplicaConfig(properties);
                break;
            case Sentinel:
                redisConfiguration = sentinelConfig(properties);
                break;
            case Replicated:
                // TODO 暂无
                break;
            default:
                throw new BadRedisException("00003", "无法识别的redis类型");
        }
        return redisConfiguration;
    }


    public static final RedisStaticMasterReplicaConfiguration staticMasterReplicaConfig(RedisPropertiesEntity properties) {
        RedisStaticMasterReplicaConfiguration config = new RedisStaticMasterReplicaConfiguration(properties.getHost(), properties.getPort());
        config.setUsername(properties.getUsername());
        config.setPassword(RedisPassword.of(properties.getPassword()));
        config.setDatabase(properties.getDatabase());
        return config;
    }


    public static final RedisStandaloneConfiguration standaloneConfig(RedisPropertiesEntity properties) {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(properties.getHost());
        config.setPort(properties.getPort());
        config.setUsername(properties.getUsername());
        config.setPassword(RedisPassword.of(properties.getPassword()));
        config.setDatabase(properties.getDatabase());
        return config;
    }

    public static final RedisSentinelConfiguration sentinelConfig(RedisPropertiesEntity properties) {
        RedisSentinelConfiguration config = new RedisSentinelConfiguration();
        config.master(properties.getMaster());
        config.setSentinels(redisNodeList(properties.getNodes()));
        config.setUsername(properties.getUsername());
        if (properties.getPassword() != null) {
            config.setPassword(RedisPassword.of(properties.getPassword()));
        }
        if (properties.getPassword() != null) {
            config.setSentinelPassword(RedisPassword.of(properties.getPassword()));
        }
        config.setDatabase(properties.getDatabase());
        return config;
    }


    public static final RedisClusterConfiguration clusterConfiguration(RedisPropertiesEntity properties) {
        RedisClusterConfiguration config = new RedisClusterConfiguration(properties.getNodes());
        if (properties.getMaxRedirects() != null) {
            config.setMaxRedirects(properties.getMaxRedirects());
        }
        config.setUsername(properties.getUsername());
        if (properties.getPassword() != null) {
            config.setPassword(RedisPassword.of(properties.getPassword()));
        }
        return config;
    }

    /*----------RedisConfiguration end----------*/


    /**
     * 生成RedisNode
     *
     * @param nodeList
     * @return
     */
    public static List<RedisNode> redisNodeList(List<String> nodeList) {
        List<RedisNode> nodes = new ArrayList<>();
        for (String node : nodeList) {
            try {
                String[] parts = StringUtil.StringUtilsBySpring.split(node, ":");
                Assert.state(parts.length == 2, "Must be defined as 'host:port'");
                nodes.add(new RedisNode(parts[0], Integer.parseInt(parts[1])));
            } catch (RuntimeException ex) {
                throw new BadRedisException("00004", "Invalid redis sentinel property '{}'", ex, node);
            }
        }
        return nodes;
    }

    /*----------RedisTemplate start----------*/

    public static final RedisTemplate<String, Object> redisTemplate(RedisPropertiesEntity properties) {
        return redisTemplate(redisConnectionFactory(properties));
    }

    public static final RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 开启redis数据库事务的支持
        redisTemplate.setEnableTransactionSupport(true);
        redisTemplate.setConnectionFactory(connectionFactory);
        // 如果不配置Serializer，那么存储的时候缺省使用String，如果用User类型存储，那么会提示错误User can't cast to String！
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        // jackson序列化对象设置
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = jackson2JsonRedisSerializer();
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /*----------RedisTemplate end----------*/


    /**
     * 序列化
     *
     * @return
     */
    public static final Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(JsonUtil.objectMapper);
        return jackson2JsonRedisSerializer;
    }

    /**
     * RedisCacheConfiguration
     *
     * @param seconds
     * @return
     */
    public static final RedisCacheConfiguration redisCacheConfiguration(Integer seconds) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = jackson2JsonRedisSerializer();
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
                RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(jackson2JsonRedisSerializer)
        ).entryTtl(Duration.ofSeconds(seconds));

        return redisCacheConfiguration;
    }

    /*----------RedisCacheManager start----------*/

    public static RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory,
                                                      RedisCacheConfiguration redisCacheConfiguration) {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        RedisCacheManager redisCacheManager
                = new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
                redisCacheConfiguration, redisCacheConfigurationMap);
        return redisCacheManager;
    }

    public static RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory,
                                                      RedisCacheConfiguration redisCacheConfiguration,
                                                      Map<String, RedisCacheConfiguration> redisCacheConfigurationMap) {
        RedisCacheManager redisCacheManager
                = new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
                redisCacheConfiguration, redisCacheConfigurationMap);
        return redisCacheManager;
    }
    /*----------RedisCacheManager end----------*/

    /*----------RedisConnectionFactory start----------*/

    public static RedisConnectionFactory redisConnectionFactory(RedisPropertiesEntity properties,
                                                                ClientResources clientResources,
                                                                GenericObjectPoolConfig genericObjectPoolConfig,
                                                                LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer) {
        LettuceClientConfiguration clientConfig
                = lettuceClientConfiguration(properties,
                clientResources,
                genericObjectPoolConfig,
                lettuceClientConfigurationBuilderCustomizer);
        return redisConnectionFactory(properties, clientConfig);
    }

    public static RedisConnectionFactory redisConnectionFactory(RedisPropertiesEntity properties,
                                                                LettuceClientConfiguration clientConfig) {
        return lettuceConnectionFactory(properties, clientConfig);
    }


    public static RedisConnectionFactory redisConnectionFactory(RedisPropertiesEntity properties,
                                                                ClientResources clientResources,
                                                                LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer) {
        return redisConnectionFactory(properties, clientResources, properties.getGenericObjectPoolConfig(), lettuceClientConfigurationBuilderCustomizer);
    }

    public static RedisConnectionFactory redisConnectionFactory(RedisPropertiesEntity properties,
                                                                GenericObjectPoolConfig genericObjectPoolConfig,
                                                                LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer) {
        return redisConnectionFactory(properties, clientResources(), genericObjectPoolConfig, lettuceClientConfigurationBuilderCustomizer);
    }

    public static RedisConnectionFactory redisConnectionFactory(RedisConfiguration redisConfiguration,
                                                                LettuceClientConfiguration clientConfiguration) {
        return lettuceConnectionFactory(redisConfiguration, clientConfiguration);
    }

    public static RedisConnectionFactory redisConnectionFactory(RedisPropertiesEntity properties,
                                                                LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer) {
        return redisConnectionFactory(properties, clientResources(), lettuceClientConfigurationBuilderCustomizer);
    }

    public static RedisConnectionFactory redisConnectionFactory(RedisPropertiesEntity properties) {
        return redisConnectionFactory(properties, clientResources(), SpringUtil.getBean(LettuceClientConfigurationBuilderCustomizer.class));
    }

    public static LettuceConnectionFactory lettuceConnectionFactory(RedisPropertiesEntity properties,
                                                                    LettuceClientConfiguration clientConfiguration) {
        RedisConfiguration redisConfiguration = redisConfiguration(properties);
        return lettuceConnectionFactory(redisConfiguration, clientConfiguration);
    }

    public static LettuceConnectionFactory lettuceConnectionFactory(RedisConfiguration redisConfiguration,
                                                                    LettuceClientConfiguration clientConfiguration) {
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisConfiguration, clientConfiguration);
        lettuceConnectionFactory.afterPropertiesSet();
        return lettuceConnectionFactory;
    }

    /*----------RedisConnectionFactory end----------*/


    public static ClientResources clientResources() {
        return ClientResources.builder().build();
    }

    /*----------LettuceClientConfiguration start----------*/

    public static LettuceClientConfiguration lettuceClientConfiguration(RedisPropertiesEntity properties,
                                                                        ClientResources clientResources,
                                                                        GenericObjectPoolConfig genericObjectPoolConfig,
                                                                        LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer) {
        LettuceClientConfiguration.LettuceClientConfigurationBuilder builder = createBuilder(genericObjectPoolConfig);
        applyProperties(properties, builder);
        if (properties.isSsl()) {
            builder.useSsl();
        }
        builder.clientOptions(createClientOptions(properties));
        builder.clientResources(clientResources);
        lettuceClientConfigurationBuilderCustomizer.customize(builder);
        return builder.build();
    }


    public static LettuceClientConfiguration.LettuceClientConfigurationBuilder createBuilder(GenericObjectPoolConfig genericObjectPoolConfig) {
        if (genericObjectPoolConfig == null) {
            return LettuceClientConfiguration.builder();
        }
        return LettucePoolingClientConfiguration.builder().poolConfig(genericObjectPoolConfig);
    }

    public static void applyProperties(RedisPropertiesEntity properties,
                                       LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) {
        if (properties.isSsl()) {
            builder.useSsl();
        }
        if (properties.getTimeout() != null) {
            builder.commandTimeout(properties.getTimeout());
        }
        if (properties.getShutdownTimeout() != null && !properties.getShutdownTimeout().isZero()) {
            builder.shutdownTimeout(properties.getShutdownTimeout());
        }
        if (StringUtil.StringUtilsBySpring.hasText(properties.getClientName())) {
            builder.clientName(properties.getClientName());
        }
    }

    public static ClientOptions createClientOptions(RedisPropertiesEntity properties) {
        ClientOptions.Builder builder = initializeClientOptionsBuilder(properties);
        Duration connectTimeout = properties.getConnectTimeout();
        if (connectTimeout != null) {
            builder.socketOptions(SocketOptions.builder().connectTimeout(connectTimeout).build());
        }
        return builder.timeoutOptions(TimeoutOptions.enabled()).build();
    }


    public static ClientOptions.Builder initializeClientOptionsBuilder(RedisPropertiesEntity properties) {
        if (RedisTypeEnum.Cluster.equals(properties.getRedisTypeEnum())) {
            ClusterClientOptions.Builder builder = ClusterClientOptions.builder();
            RedisPropertiesEntity.Refresh refreshProperties = properties.getRefresh();
            ClusterTopologyRefreshOptions.Builder refreshBuilder = ClusterTopologyRefreshOptions.builder()
                    .dynamicRefreshSources(refreshProperties.isDynamicRefreshSources());
            if (refreshProperties.getPeriod() != null) {
                refreshBuilder.enablePeriodicRefresh(refreshProperties.getPeriod());
            }
            if (refreshProperties.isAdaptive()) {
                refreshBuilder.enableAllAdaptiveRefreshTriggers();
            }
            return builder.topologyRefreshOptions(refreshBuilder.build());
        }

        return ClientOptions.builder();
    }

    /*----------LettuceClientConfiguration end----------*/


    /**
     * Inner class to allow optional commons-pool2 dependency.
     */
    public static class PoolBuilderFactory {

        LettuceClientConfiguration.LettuceClientConfigurationBuilder createBuilder(org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool properties) {
            return LettucePoolingClientConfiguration.builder().poolConfig(getPoolConfig(properties));
        }

        public GenericObjectPoolConfig<?> getPoolConfig(org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool properties) {
            GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
            config.setMaxTotal(properties.getMaxActive());
            config.setMaxIdle(properties.getMaxIdle());
            config.setMinIdle(properties.getMinIdle());
            if (ObjectUtil.ObjectUtilByLang3.isNotEmpty(properties.getTimeBetweenEvictionRuns())) {
                config.setTimeBetweenEvictionRuns(properties.getTimeBetweenEvictionRuns());
            }
            if (ObjectUtil.ObjectUtilByLang3.isNotEmpty(properties.getMaxWait())) {
                config.setMaxWait(properties.getMaxWait());
            }
            return config;
        }

    }

    /*----------Config start----------*/

    public static final Config config(RedisPropertiesEntity properties) {
        final Config config = new Config();
        config.setCodec(new org.redisson.client.codec.StringCodec());
        switch (properties.getRedisTypeEnum()) {
            case Single:
                configSingel(config, properties);
                break;
            case Cluster:
                configCluster(config, properties);
                break;
            case MasterSlave:
                configMasterSlave(config, properties);
                break;
            case Sentinel:
                configSentinel(config, properties);
                break;
            case Replicated:
                configReplicated(config, properties);
                break;
            default:
                throw new BadRedisException("00005", "无法识别的redis类型");
        }

        return config;
    }

    public static void configReplicated(final Config config, RedisPropertiesEntity properties) {
        config.useReplicatedServers()
                .setDatabase(properties.getDatabase())
                .setUsername(properties.getUsername())
                .setPassword(properties.getPassword())
                .setNodeAddresses(properties.getNodes(REDIS_PROTOCOL_PREFIX));

    }

    public static void configSentinel(final Config config, RedisPropertiesEntity properties) {
        config.useSentinelServers()
                .setDatabase(properties.getDatabase())
                .setUsername(properties.getUsername())
                .setPassword(properties.getPassword())
                .setMasterName(properties.getClientName())
                .setSentinelAddresses(properties.getNodes(REDIS_PROTOCOL_PREFIX));
    }

    public static void configMasterSlave(final Config config, RedisPropertiesEntity properties) {
        config.useMasterSlaveServers()
                .setDatabase(properties.getDatabase())
                .setUsername(properties.getUsername())
                .setPassword(properties.getPassword())
                .setMasterAddress(properties.getAddress(REDIS_PROTOCOL_PREFIX))
                .setClientName(properties.getClientName())
                .setSlaveAddresses(new HashSet<>(properties.getNodes(REDIS_PROTOCOL_PREFIX)));
    }

    public static void configCluster(final Config config, RedisPropertiesEntity properties) {
        config.useClusterServers()
                .setPassword(properties.getPassword())
                .setUsername(properties.getUsername())
                .setNodeAddresses(properties.getNodes(REDIS_PROTOCOL_PREFIX));
    }


    public static void configSingel(final Config config, RedisPropertiesEntity properties) {
        config.useSingleServer()
                .setDatabase(properties.getDatabase())
                .setAddress(properties.getAddress(REDIS_PROTOCOL_PREFIX))
                .setUsername(properties.getUsername())
                .setPassword(properties.getPassword());
    }

    /*----------Config end----------*/

    /*----------RedissonClient start----------*/

    public static RedissonClient redisson(Config config) {
        return Redisson.create(config);
    }

    public static RedissonClient redisson(RedisPropertiesEntity properties) {
        return Redisson.create(config(properties));
    }

    public static RedissonRxClient redissonRx(RedisPropertiesEntity properties) {
        return redisson(properties).rxJava();
    }

    public static RedissonRxClient redissonRx(Config config) {
        return redisson(config).rxJava();
    }

    public static RedissonRxClient redissonRx(Redisson redisson) {
        return redisson.rxJava();
    }

    /*----------RedissonClient end----------*/


}
